#!/usr/bin/lua

-- -----------------------------------------------------------------
-- SNXG와 MF2 간의 정책 변환이 제대로 이뤄졌는지를 확인하기 위한 툴
--  정책변환기의 결과를 확인하기 위해서도 사용할 수 있을 듯..
--  ipfw shows와 fw show srule low를 비교하고, fw show srule이 약간 도움
-- 
--  주로 파싱해서 객체에 넣는 부분에서 필요한 normalize를 수행함.
--  왜냐하면 비교할 때의 로직은 좀 복잡해 질 수 있으므로
--  특별히 할 일이 없는 객체로 밀어넣는 부분에서 많은 일을 하도록 함.
-- -----------------------------------------------------------------

-- 스트링을 특정한 문자들로 자르는 함수
function split(str, pat)
    local t = {}
    local fpat = "(.-)" .. pat
    local last_end = 1
    local s, e, cap = str:find(fpat, 1)
    while s do
	if s ~= 1 or cap ~= "" then
	    table.insert(t,cap)
	end
	last_end = e+1
	s, e, cap = str:find(fpat, last_end)
    end
    if last_end <= #str then
	cap = str:sub(last_end)
	table.insert(t, cap)
    end
    return t
end

-- 중복되는 포트를 제거하기 위해서 소트하고 바로 뒤의 엔트리가 같은 값이면 제거
function remove_duplicate (list)
    table.sort(list)
    local n = #list
    local i = 1
    while i <= n-1 do
	if list[i] == list[i + 1] then
	    table.remove(list, i + 1)
	    n = n - 1
	else
	    i = i + 1
	end
    end
    return list
end

-- 가장 넓은 범위의 네트워크부터 자신에게 포함되는 애들을 제거하도록...
function eliminate_subnetwork (list)
    -- 이 함수는 안 쓰임.
    -- Lua에 bit operator가 없어서 못 씀
    -- 그냥 base가 제대로 되어있다고 믿기로 함.
    function get_mask (mask)
        local t = { 0x80000000, 0xC0000000, 0xE0000000, 0xF0000000,
                    0xF8000000, 0xFC000000, 0xFE000000, 0xFF000000,
                    0xFF800000, 0xFFC00000, 0xFFE00000, 0xFFF00000,
                    0xFFF80000, 0xFFFC0000, 0xFFFE0000, 0xFFFF0000,
                    0xFFFF8000, 0xFFFFC000, 0xFFFFE000, 0xFFFFF000,
                    0xFFFFF800, 0xFFFFFC00, 0xFFFFFE00, 0xFFFFFF00,
                    0xFFFFFF80, 0xFFFFFFC0, 0xFFFFFFE0, 0xFFFFFFF0,
                    0xFFFFFFf8, 0xFFFFFFFC, 0xFFFFFFFE, 0xFFFFFFFF, }
        return t[mask]
    end

    -- bit operator가 없어서 맨 마지막 주소를 계산하기 위해서 고안
    -- 특정 비트 마스크에서 주소의 수
    function get_range (mask)
        local t = { 0x7FFFFFFF, 0x3FFFFFFF, 0x1FFFFFFF, 0xFFFFFFF,
                    0x7FFFFFF,  0x3FFFFFF,  0x1FFFFFF,  0xFFFFFF,
                    0x7FFFFF,   0x3FFFFF,   0x1FFFFF,   0xFFFFF,
                    0x7FFFF,    0x3FFFF,    0x1FFFF,    0xFFFF,
                    0x7FFF,     0x3FFF,     0x1FFF,     0xFFF,
                    0x7FF,      0x3FF,      0x1FF,      0xFF,
                    0x7F,       0x3F,       0x1F,       0xF,
                    0x7,        0x3,        0x1,        0, }
        return t[mask]
    end

    -- 스트링을 .(.을 .으로만 넘기면 오류, %로 escape)으로 나누고
    -- 각 segment에 수를 곱해서... bit operator가 없어서 좀 허접
    function addr_aton (addr_string)
        local t = split(addr_string, '%.')
        local n
        if #t ~= 4 then
            print("invalid addr", addr_string, #t)
            return 0
        end
        n = tonumber(t[1]) * 0x1000000 + tonumber(t[2]) * 0x10000 +
            tonumber(t[3]) * 0x100 + tonumber(t[4]) 
        return n
    end

    -- mask : 0~31 for masked network
    -- mask = 32 for ranged network
    -- mask = 33 for host
    function mask_start_end (addr)
        local slash = string.find(addr, '/')
        local mask, base
        local start_addr, end_addr
        if slash then -- CIDR의 경우
            mask = tonumber(string.sub(addr, slash + 1))
            base = addr_aton(string.sub(addr, 1, slash - 1))
            start_addr = base -- 이걸 믿어야 하나? 루아가 비트연산이 없네?
            end_addr = base + get_range(mask)
        else
            local dash = string.find(addr, '-')
            if dash then -- Range의 경우
                mask = 32
                start_addr = addr_aton(string.sub(addr, 1, dash - 1))
                end_addr = addr_aton(string.sub(addr, dash + 1))
            else  -- 호스트의 경우
                mask = 33
                start_addr = addr_aton(addr)
                end_addr = start_addr
            end
        end
        return mask, start_addr, end_addr
    end

    -- prefix가 가장 짧은 놈이 리스트의 제일 앞에 서도록
    -- 범위로 된 것들이 그 다음으로 오도록
    -- 맨 마지막으로 독립군...
    function shortest_one(a, b) 
        local a_mask, a_start, a_end = mask_start_end(a)
        local b_mask, b_start, b_end = mask_start_end(b)

        if a_mask == b_mask then
            if a_start == b_start then
                return a_end < b_end
            else
                return a_start < b_start
            end
        else
            return a_mask < b_mask
        end
    end

    -- container가 containee를 포함하는지 확인
    function is_containment(container, containee)
        local er_mask, er_start, er_end = mask_start_end(container)
        local ee_mask, ee_start, ee_end = mask_start_end(containee)
        
        -- 컨테이너가 범위가 더 넓은지 검사
        if (er_start <= ee_start) and (er_end >= ee_end) then
            return true
        else
            return false
        end
    end

    -- sorting을 수행
    table.sort(list, shortest_one)
    -- 맨 앞의 엔트리부터 뒤의 애들을 stamping
    local n = #list
    local i = 1
    while i <= n-1 do
        j = i + 1
        while j <= n do
            if is_containment(list[i], list[j]) then
                table.remove(list, j)
                n = n - 1 -- j가 지워지면 밀려서 자동적으로 다음게 오므로
            else
                j = j + 1 -- 지울 필요가 없을 때만 j를 하나씩 늘림
            end
        end
        i = i + 1
    end
    return list
end

-- zone 표시 방식을 normalize
-- MF2 방식으로 " "로 된 부분을 "|"로 바꿈
function normalize_zone(zone)
    return table.concat(split(zone, " "), '|')
end

-- 정책의 중복되지 않는 부분을 담는 Class
-- 보통 정책 ID, action은 중복되지만 from, to, protocol, port number, zone은
-- 중복되지 않기 때문에 각각을 정책 내의 리스트에 저장한다.
Contents = {
}

function Contents:new (o)
    o = o or {}
    setmetatable(o, self)
    self.__index = self
    return o
end

-- 매우 간략화된 정책
-- ID, action, 내용 리스트의 3 요소를 가진다.
-- ID는 정책 ID로 Uniq한 ID
-- action은 정책마다 하나만 존재할 수 있다.
-- 내용 리스트는 Contents Class 객체가 여러 개 올 수 있다.
-- MF2의 정책 보기에 시간 옵션이 나오므로 이 부분을 추가.
Rule = {
    id = 0,
}

function Rule:new (o)
   o = o or {}
   setmetatable(o, self)
   self.__index = self
   return o
end

function Rule:set_id (rule_id)
    self.id = rule_id
end

function Rule:set_action (action)
    if not self.action or self.action == "" then
	self.action = action
    elseif self.action ~= action then
	print("More than one action for a Rule ", self.id)
    end
end

function Rule:set_contents (from_str, to_str, port_str, protocol, zone)
    local t = Contents:new({ from = remove_duplicate(split(from_str, ",")), 
			     to = remove_duplicate(split(to_str, ",")),
			     port = remove_duplicate(split(port_str, ",")),
			     protocol = protocol, 
			     zone = normalize_zone(zone) })
    local s = eliminate_subnetwork(t.from)
    if not self.contents then
	self.contents = {}
    end
    table.insert(self.contents, t)
end

function Rule:set_timeoption(time_option)
end

-- 이름 그대로... SNXG에서 ipfw shows로 출력되는 결과를 parsing.
-- 첫번째 argument는 파일이름
-- 두번째는 파싱 결과로 정책이 저장될 테이블 
function parse_snxg_rule (snxg_rule_dump, rule_table)
    local f = io.open(snxg_rule_dump)
    if not f then
        print(string.format("Failed to open file: %s", snxg_rule_dump))
        return nil
    end
    for line in f:lines() do
        -- regular expression을 이용해서 정책을 해석
        local seq_str, pkt_cnt_str, byt_cnt_str, id_str, action, from_str, to_str, protocol, port_num_str, zone 
            = string.match(line, "(%d+)%s+(%d+)%s+(%d+)%s+(%d+) (%l+) ([0-9.,-/]+) %-> ([0-9.,-/]+) %((.+)%) ([0-9,-]+) ([%u ]+).*")
        if zone then -- 맨 마지막 것까지 변화되었으면
            -- print(seq_str, pkt_cnt_str, byt_cnt_str, id_str, action, from_str, to_str, protocol, port_num_str, zone)
            id = tonumber(id_str)
            rule = rule_table[id]
            if rule then
		rule:set_action(action)
		rule:set_contents(from_str, to_str, port_num_str, protocol, zone)
            else
                rule = Rule:new()
                rule:set_id(id)
                rule:set_action(action)
		rule:set_contents(from_str, to_str, port_num_str, protocol, zone)
                rule_table[id] = rule
            end
        else
            print("Unable to parse:", line)
        end
    end
    f:close()
    return rule_table
end

-- MF2 low rule을 parsing함.
-- 패턴만 다르지 근본적으로는 동일한 방식임.
function parse_mf2_low_rule (mf2_rule_dump, rule_table)
    local f = io.open(mf2_rule_dump)
    if not f then
        return nil
    end
    for line in f:lines() do
        local num_str, seq_str, pkt_cnt_str, id_str, action, from_str, to_str, protocol, port_num_str, zone 
            = string.match(line, "%[([ %d]+)%]%s+(%d+)%s+(%d+)%s+(%d+) (%l+) ([0-9.,-/]+) %-> ([0-9.,-/]+) svc%((%w+) 0%-65535 ([^)]*)%) zone%(([%u|]+)%).*")
	if not zone then
	    -- another try : 서비스가 any면 위의 방법으로 잘 안 되어서...
	    num_str, seq_str, pkt_cnt_str, id_str, action, from_str, to_str, zone 
                = string.match(line, "%[([ %d]+)%]%s+(%d+)%s+(%d+)%s+(%d+) (%l+) ([0-9.,-/]+) %-> ([0-9.,-/]+) svc%(any%) zone%(([%u|]+)%).*")
	    protocol = "ip"
	    port_num_str = "0-65535"
	end
        if zone then 
            -- print(num_str, action, port_num_str, zone)
	    id = tonumber(id_str)
	    rule = rule_table[id]
	    if rule then
		rule:set_action(action)
		rule:set_contents(from_str, to_str, port_num_str, protocol, zone)
	    else
		rule = Rule:new()
		rule:set_id(id)
		rule:set_action(action)
		rule:set_contents(from_str, to_str, port_num_str, protocol, zone)
		rule_table[id] = rule
	    end
        else
            print("Unable to parse:", line)
        end
    end
    f:close()
end

-- MF2의 fw show srule의 결과를 파싱
-- 이걸 SNXG랑 직접 비교하기는 힘듦. 기계 보다는 사람에 중심을 둔 정책 보기 방식이라서...
-- 여기서는 time option에 대한 정보를 얻을 수 있음.
function parse_mf2_rule (mf2_rule_dump, rule_table)
    local f = io.open(mf2_rule_dump)
    if not f then
        return nil
    end
    for line in f:lines() do
        local num_str, seq_str, hit_cnt_str, pkt_cnt_str, byt_cnt_str, id_str, action, from_str, to_str, svc_str, zone, time_opt_str 
            = string.match(line, "%[([ %d]+)%]%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+) (%l+) ([0-9.,-/]+) %-> ([0-9.,-/]+) svc%((.-)%) zone%(([%u|]+)%) time%((%l+)%).*")
        if time_opt_str then 
            -- print(num_str, action, to_str, svc_str, zone, time_opt_str)
	    id = tonumber(id_str)
	    rule = rule_table[id]
	    if rule then
                -- 시간 옵션을 설정
                rule:set_timeoption(time_opt_str)
	    else
		print("Not Found rule", id)
	    end
        else
            print("Unable to parse:", line)
        end
    end
    f:close()
end

-- 정책 내에 있는 from, to와 같은 IP list를 비교
function compare_ip_list(list1, list2)
    if #list1 ~= #list2 then
	return false
    end

    for i, v in ipairs(list1) do
	if v ~= list2[i] then
	    return false
	end
    end
    return true
end

-- 정책에 속한 port list를 비교
function compare_port_list(list1, list2)
    if #list1 ~= #list2 then -- 일단 숫자가 다르면 무조건 다르다.
	return false
    end

    for i, v in ipairs(list1) do
	if v ~= list2[i] then
	    return false
	end
    end
    return true
end

-- zone을 비교
function compare_zone(zone1, zone2)
    if zone1 == zone2 then -- 일단 스트링끼리 비교
	return true
    else
        return false
    end
end

-- 정책들에 있는 from, to, 
function compare_contents(con1, con2)
    if not compare_zone(con1.zone, con2.zone) then
	print(string.format("    Diff:  [%s] <> [%s]", 
			    con1.zone, con2.zone))
	return false
    else
	print(string.format("    Match: [%s] == [%s]",
			    con1.zone, con2.zone))
    end

    if con1.protocol ~= con2.protocol then
	print(string.format("    Diff:  [%s] <> [%s]", 
			    con1.protocol, con2.protocol))
        return false
    else
	print(string.format("    Match: [%s] == [%s]", 
			    con1.protocol, con2.protocol))
    end

    if not compare_port_list(con1.port, con2.port) then
	print(string.format("    Diff:  [%s] <> [%s]",
			    table.concat(con1.port, ', '),
			    table.concat(con2.port, ', ')))
	return false
    else
	print(string.format("    Match: [%s] == [%s]",
			    table.concat(con1.port, ', '),
			    table.concat(con2.port, ', ')))
    end

    if not compare_ip_list(con1.from, con2.from) then
	print(string.format("    Diff:  [%s] <> [%s]", 
			    table.concat(con1.from, ', '), 
			    table.concat(con2.from, ', ')))
	return false
    else
	print(string.format("    Match: [%s] == [%s]", 
			    table.concat(con1.from, ', '), 
			    table.concat(con2.from, ', ')))
    end
    if not compare_ip_list(con1.to, con2.to) then
	print(string.format("    Diff:  [%s] <> [%s]", 
			    table.concat(con1.to, ', '),
			    table.concat(con2.to, ', ')))
	return false
    else
	print(string.format("    Match: [%s] == [%s]", 
			    table.concat(con1.to, ', '),
			    table.concat(con2.to, ', ')))
    end
    return true
end

function compare_rules(table1, table2)
    local not_matching_rules = {}
    for k1, v1 in pairs(table1) do
	local v2 = table2[k1]
	if v2 then
	    print(string.format("Comparing rule ID %d...", k1))
	    if v1.action ~= v2.action then
		print(string.format("  Rule ID %d's actions do not match. (%s, %s)", k1, v1.action, v2.action))
	    else
		print(string.format("  Action Matched: %s <==> %s", v1.action, v2.action))
	    end
	    if #v1.contents ~= #v2.contents then
		print(string.format("  Rule ID %d's count of from-to-services do not match.: %d <> %d", 
				    k1, #v1.contents, #v2.contents))
	    else
		print(string.format("  Count Matched:  %d <==> %d", #v1.contents, #v2.contents))
	    end
	    local matched = true
	    for i, con1 in ipairs(v1.contents) do
		local matched_1 = false
		for j, con2 in ipairs(v2.contents) do
		    print(string.format("  v1.contents[%d] vs v2.contents[%d]", 
					i, j))
		    if compare_contents(con1, con2) then
			matched_1 = true
			break
		    end
		end
		if not matched_1 then
		    matched = false
		end
	    end
	    if not matched then
		print("  Content Matched: False")
                table.insert(not_matching_rules, k1)
	    else
		print("  Content Matched: True")
	    end
	else
	    print(string.format("Rule ID %d is missing in MF2 Table", k1))
	end
    end
    for k2, v2 in pairs(table2) do
	local v1 = table1[k2]
	if v1 then
	else
	    print(string.format("Rule ID %d is missing in SNXG Rule Table", k2))
	end
    end
    print("List of not-matched-rule :", 
          table.concat(not_matching_rules, ", "))
end

function main()
    -- 정책들을 모두 저장하는 테이블
    -- SNXG용과 MF2용이 각각 있음.
    MF2_RuleTable = {}
    SNXG_RuleTable = {}

    -- Parse Rule Dumps
    parse_snxg_rule("NXG_RULE.txt", SNXG_RuleTable)
    parse_mf2_low_rule("MF2_RULE_LOW.txt", MF2_RuleTable)
    parse_mf2_rule("MF2_RULE.txt", MF2_RuleTable)

    -- Compare tables
    compare_rules(SNXG_RuleTable, MF2_RuleTable)
end

main()